﻿// <copyright file="DeadSimpleLoader.cs" company="Sky Sanders">
// This source is subject to the Microsoft Public License (Ms-PL).   
// Please see http://deadsimpledto.codeplex.com/license for details.   
// All other rights reserved.  
// </copyright> 
// <version>2.04 beta</version>

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Data.Linq.Mapping;
using System.Data.SqlClient;
using System.Linq;
using System.Reflection;
using System.Text;

namespace DeadSimpleDTO
{
    /// <summary>
    /// Dead Simple DTO 2 Reflective loader.
    /// 
    /// </summary>
    /// 
    public class DeadSimpleLoader
    {
        private const string StrDsdto2ConnectionString = "DSDTO2-ConnectionString";
        private static readonly Dictionary<Type, TableInfo> ReflectionCache = new Dictionary<Type, TableInfo>();
        private static string _cstring;

        public DeadSimpleLoader()
        {
        }

        public DeadSimpleLoader(string connectionString)
        {
            ConnectionString = connectionString;
        }


        public static string ConnectionString
        {
            get
            {
                if (string.IsNullOrEmpty(_cstring))
                {
                    _cstring = GetConnectionString(StrDsdto2ConnectionString);
                }

                return _cstring;
            }

            set { _cstring = GetConnectionString(value); }
        }

        public IList<T> Get<T>() where T : new()
        {
            return Select<T>(null);
        }

        private static void ValidateArgs(TableInfo tinfo, params object[] keys)
        {
            string message = string.Join(", ",
                                         tinfo.Keys.Select(k => String.Format("{0} {1}", k.PropertyType.Name, k.Name)).
                                             ToArray());

            if (keys.Length != tinfo.Keys.Length)
            {
                throw new ArgumentOutOfRangeException(String.Format("Parameter count: Expected {0}", message));
            }

            for (int i = 0; i < keys.Length; i++)
            {
                if (keys[i].GetType() != tinfo.Keys[i].PropertyType)
                {
                    throw new ArgumentOutOfRangeException(String.Format("Parameter type: Expected {0}", message));
                }
            }
        }

        /// <summary>
        /// Returns a hydrated instance of T if a matching record is found 
        /// otherwise null.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="keys">Primary key values</param>
        /// <returns></returns>
        public T Get<T>(params object[] keys) where T : new()
        {
            TableInfo tinfo;

            if (!ReflectionCache.ContainsKey(typeof(T)))
            {
                tinfo = new TableInfo(typeof(T));
                ReflectionCache.Add(tinfo.Type, tinfo);
            }

            tinfo = ReflectionCache[typeof(T)];

            ValidateArgs(tinfo, keys);


            T returnValue = default(T);

            using (var conn = new SqlConnection(ConnectionString))
            {
                using (SqlCommand cmd = tinfo.Command.Clone())
                {
                    cmd.CommandText = tinfo.GetStatement;

                    for (int i = 0; i < tinfo.Keys.Length; i++)
                    {
                        cmd.Parameters[i].Value = keys[i];
                    }


                    cmd.Connection = conn;
                    cmd.Connection.Open();

                    using (SqlDataReader reader = cmd.ExecuteReader())
                    {
                        if (reader == null)
                        {
                            throw new DataException("failed to get reader");
                        }
                        if (reader.Read())
                        {
                            returnValue = new T();
                            Load(tinfo.Columns, returnValue, reader);
                            reader.Close();
                        }
                    }
                    cmd.Connection.Close();
                }
            }
            return returnValue;
        }

        /// <summary>
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <returns></returns>
        public List<T> Select<T>() where T : new()
        {
            return Select<T>(null);
        }

        /// <summary>
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="where"></param>
        /// <returns></returns>
        public List<T> Select<T>(string where) where T : new()
        {
            // check for attribute on T

            TableInfo tinfo;
            if (!ReflectionCache.ContainsKey(typeof(T)))
            {
                tinfo = new TableInfo(typeof(T));
                ReflectionCache.Add(tinfo.Type, tinfo);
            }
            tinfo = ReflectionCache[typeof(T)];

            var returnValue = new List<T>();

            using (var conn = new SqlConnection(ConnectionString))
            {
                using (SqlCommand cmd = conn.CreateCommand())
                {
                    cmd.CommandText = tinfo.SelectStatement + " " + (string.IsNullOrEmpty(where) ? "" : " where " + where);
                    cmd.Connection = conn;
                    cmd.Connection.Open();
                    SqlDataReader reader = cmd.ExecuteReader();
                    if (reader == null)
                    {
                        throw new DataException("failed to get reader");
                    }
                    while (reader.Read())
                    {
                        var dto = new T();
                        Load(tinfo.Columns, dto, reader);
                        returnValue.Add(dto);
                    }
                    cmd.Connection.Close();
                }
            }
            return returnValue;
        }

        #region Private Implementation Details

        /// <summary>
        /// Allows passing a connection string, an appSettings key that contains
        /// either a connection string or a connectionStrings name, or a connectionStrings
        /// element key.
        /// </summary>
        /// <param name="connectionString"></param>
        /// <returns></returns>
        private static string GetConnectionString(string connectionString)
        {
            if (ConfigurationManager.AppSettings[connectionString] != null)
            {
                connectionString = ConfigurationManager.AppSettings[connectionString];
            }

            if (ConfigurationManager.ConnectionStrings[connectionString] != null)
            {
                connectionString = ConfigurationManager.ConnectionStrings[connectionString].ConnectionString;
            }
            return connectionString;
        }

        /// <summary>
        /// Loads a DTO from an open and positioned reader.
        /// </summary>
        /// <param name="columns">A Dictionary of column PropertyInfo and ColumnAttributes</param>
        /// <param name="obj">An object with properties corellating with the columns collection</param>
        /// <param name="reader">open datarecord positioned to the record to read</param>
        private static void Load(Dictionary<PropertyInfo, ColumnAttribute> columns, object obj, IDataRecord reader)
        {
            foreach (PropertyInfo pinfo in columns.Keys)
            {
                ColumnAttribute columnAttribute = columns[pinfo];

                // pull the value for the property from the reader
                object value = reader[reader.GetOrdinal(columnAttribute.Name)];

                // push the value into the property
                pinfo.SetValue(obj, GetValueOrDefault(pinfo, value), null);
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="pinfo"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        private static object GetValueOrDefault(PropertyInfo pinfo, object value)
        {
            return value == DBNull.Value
                // is dbnull
                       ? (pinfo.PropertyType.IsValueType
                              ?
                // is value type, createInstance will always work
                          Activator.CreateInstance(pinfo.PropertyType)
                              :
                // is reference type, return null
                          null)
                // not dbnull - return value
                       : value;
        }

        #endregion

        #region Nested type: TableInfo

        private class TableInfo
        {
            public TableInfo(Type dtoType)
            {
                var att =
                    (TableAttribute)
                    dtoType.GetCustomAttributes(true).ToList().FirstOrDefault(
                        a => a.GetType() == typeof(TableAttribute));


                if (att == null)
                {
                    throw new ArgumentException("Argument must be a dto type", "dtoType");
                }

                Header = att;

                Type = dtoType;

                BuildColumns(dtoType);

                BuildSelect();
            }

            public Dictionary<PropertyInfo, ColumnAttribute> Columns { get; private set; }
            public SqlCommand Command { get; private set; }
            public string GetStatement { get; private set; }
            public TableAttribute Header { get; private set; }
            public PropertyInfo[] Keys { get; private set; }
            public string SelectStatement { get; private set; }
            public Type Type { get; private set; }

            private void BuildColumns(Type dtoType)
            {
                Columns = new Dictionary<PropertyInfo, ColumnAttribute>();
                foreach (PropertyInfo prop in dtoType.GetProperties())
                {
                    var a =
                        (ColumnAttribute)
                        prop.GetCustomAttributes(true).ToList().FirstOrDefault(
                            x => x.GetType() == typeof(ColumnAttribute));
                    if (a != null)
                    {
                        Columns.Add(prop, a);
                    }
                }
                Keys = Columns.Where(p => p.Value.IsPrimaryKey).Select(p => p.Key).ToArray();
            }

            private void BuildSelect()
            {
                var sb = new StringBuilder();
                sb.AppendFormat("SELECT {0} FROM {1}",
                                string.Join(",", Columns.Values.Select(a => string.Format("[{0}]", a.Name)).ToArray()),
                                Header.Name);

                SelectStatement = sb.ToString();


                sb.Append(" WHERE ");
                Command = new SqlCommand();

                PropertyInfo[] keys = Columns.Where(p => p.Value.IsPrimaryKey).Select(p => p.Key).ToArray();

                for (int i = 0; i < keys.Length; i++)
                {
                    Command.Parameters.AddWithValue("@p" + i, null);
                    sb.AppendFormat("[{0}]=@p{1}", Columns[keys[i]].Name, i);
                    if (i < keys.Length - 1)
                    {
                        sb.Append(" AND ");
                    }
                }

                GetStatement = sb.ToString();
            }
        }

        #endregion
    }
}